﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace Concat
{
    static class BuiltinCreator
    {
        /// <summary>
        /// Creates a series of BuiltinWords from a host object.
        /// </summary>
        /// <param name="host">The object with the methods to make BuiltinWords from.</param>
        /// <param name="includeInherited">Allow inherited methods to be included as BuiltinWords.</param>
        /// <returns>A series of BuiltinWords.</returns>
        [System.Diagnostics.DebuggerNonUserCode]
        public static Tuple<string, BuiltinWord>[] getDefs(Interpreter interpreter, 
            object host, bool includeInherited = false)
        {
            // list of names and created words
            List<string> names = new List<string>();
            List<BuiltinWord> words = new List<BuiltinWord>();

            Type type = host.GetType();

            foreach (MethodInfo method in type.GetMethods())
            {
                // don't include inherited methods (unless we were told to)
                if (method.DeclaringType != type && !includeInherited)
                    continue;
                // don't include private methods
                if (!method.IsPublic)
                    continue;

                // the name to use in the interpreter
                string name = method.Name;
                bool ignore = false;
                bool enumerate = false;
                // go through each attribute
                foreach (Attribute attribute in method.GetCustomAttributes(false))
                {
                    if (attribute is BuiltinIgnoreAttribute)
                    {
                        // ignore it!
                        ignore = true;
                        break;
                    }
                    else if (attribute is BuiltinRenameAttribute)
                        // set the name to the specified name
                        name = ((BuiltinRenameAttribute)attribute).name;
                    else if (attribute is BuiltinMultiReturnAttribute)
                        // enumerate all output data
                        enumerate = true;
                }
                // there was a BuiltinIgnore attribute on the method
                if (ignore) continue;

                ParameterInfo[] parameters = method.GetParameters();
                Type returnType = method.ReturnType;
                // can we enumerate through the return values?
                bool enumerable = returnType.GetInterfaces().Contains(typeof(IEnumerable<object>));
                // make sure that if we say to enumerate results, that we can
                if (enumerate && !enumerable)
                    throw new Exception("Method return type must implement IEnumerable<object>.");
                // does the method return void?
                bool _void = returnType == typeof(void);

                // no modifiers allowed! check for params, in, out, and optional
                if (parameters.Length > 0 && isParams(parameters[parameters.Length - 1]))
                    throw new Exception("Parameter modifiers are not allowed, method '" + name + "'.");
                foreach (ParameterInfo param in parameters)
                    if (param.IsIn || param.IsOptional || param.IsOut)
                        throw new Exception("Parameter modifiers are not allowed, method '" + name + "'.");

                // copy the MethodInfo, because otherwise we close on the 'method' _variable_,
                //   not the _value_
                MethodInfo methodCopy = method;
                // the builtin word has the parameter names same as the method,
                //   and delegates the actual work to the method
                BuiltinWord word = new BuiltinWord(parameters.map(param => param.Name).ToArray(), args =>
                {
                    try
                    {
                        object returnVal = methodCopy.Invoke(host, args);
                        // return all the computed values if we are enumerating them
                        if (enumerate) return ((IEnumerable<object>)returnVal).ToArray();
                        // void methods will not return any values
                        else if (_void) return new object[0];
                        // any other return type will be turned into an array
                        else return new object[1] { returnVal };
                    }
                    catch (TargetInvocationException e)
                    {
                        throw e.InnerException;
                    }
                    catch (ArgumentException)
                    {
                        throw new InvalidArgTypesException(name, args.map(arg => arg.GetType()).ToArray(),
                            interpreter.getCallStack());
                    }
                });

                names.Add(name);
                words.Add(word);
            }

            // zip the names and words together to make a nice little package
            return names.zipWith(words).ToArray();
        }

        // whether or not the parameter was specified with params
        static bool isParams(ParameterInfo param)
        {
            return param.GetCustomAttributes(typeof(ParamArrayAttribute), false).Length > 0;
        }
    }

    /// <summary>
    /// Specifies that the method is not to be included as a builtin.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    class BuiltinIgnoreAttribute : Attribute
    {
    }

    /// <summary>
    /// Specifies a new name for a builtin method.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    class BuiltinRenameAttribute : Attribute
    {
        public string name;

        public BuiltinRenameAttribute(string name)
        {
            this.name = name;
        }
    }

    /// <summary>
    /// Specifies that the result of the method should be interpreted as multiple return values.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    class BuiltinMultiReturnAttribute : Attribute
    {
    }
}
